/**********************************************web安全方面 ************************************/

// 
// 
/**
 * XSS注入的方式：
 * 在 HTML 中内嵌的文本中，恶意内容以 script 标签形成注入
 * 在内联的 JavaScript 中，拼接的数据突破了原本的限制（字符串，变量，方法名等）
 * 在标签属性中，恶意内容包含引号，从而突破属性值的限制，注入其他属性或者标签
 * 在标签的 href、src 等属性中，包含 javascript: 等可执行代码
 * 在 onload、onerror、onclick 等事件中，注入不受控制代码
 * 在 style 属性和标签中，包含类似 background-image:url("javascript:..."); 的代码（新版本浏览器已经可以防范）
 * 在 style 属性和标签中，包含类似 expression(...) 的 CSS 表达式代码（新版本浏览器已经可以防范）
 * 
 * 一、XSS攻击防御办法：前端通过将用户输入转换为文本
 * 1、通过escapeHTML(value) 将用户输入的特殊字符进行转义，如下：
 * & => &amp;
 * < =>	&lt;
 * > =>	&gt;
 * " =>	&quot;
 * ' =>	&#x27;
 * / =>	&#x2F;
 * 
 * 2、通过白名单过滤的方式
 *  禁止掉 "javascript:" 链接、非法 scheme 等
 * 
 * 3、escapeEmbedJSON() 函数，对内联 JSON 进行转义
 * U+2028 =>	\u2028
 * U+2029 =>	\u2029
 * < =>	\u003c
 */

/**********************************************对象方法的解释说明 ************************************/

/**
 * 对象和数组，函数之间的关系：
 * Array.prototype.__proto__ 继承了 Object.prototype
 * Function.prototype.__proto__ 继承了 Object.prototype
 * 其中，Object.prototype.__proto__ 为 null。
 * 
 * 判断对象关系的几种方式说明：其核心是通过__proto__来判断 
 * 一、instanceof 是判断一个实例是否属于某种类型
 * instanceof的高级用法概括如下:
 * 1.所有对象和函数 instanceof Object  //true     （因为JS万物皆对象，函数也是对象, null instanceof Object //false ）
 * 2.所有函数 instanceof Function  //true      （这个很好理解，包括普通函数和构造函数）
 * 3.除Object和Function之外的构造函数 instanceof 自身  //false （因为构造函数的原型链上只有Function.prototype和Object.prototype而没有它们自身的prototype，这一点很不容易理解！）
 * 
 * 二、typeof: 在判断引用类型时，只会返回object, 其返回结果一共有6种：number、boolean、string、object、undefined、function(均小写)
 * 1、typeof undefined; //返回 undefined（对于基本类型，除 null 以外，均可以返回正确的结果）
 * 2、typeof null || [] || new Date() || new RegExp(); //均返回 object  
 * 3、typeof new Function(); // 返回 function (对于引用类型，除 function 以外，一律返回 object 类型)
 * 
 * 三、 Array.isArray()：判断是否是数组，其底层是通过 Object.prototype.toString.call(arg) === '[object Array]' 实现的
 * 
 */

/**
 *  in, hasOwnPrepertyOf(), propertyIsEnumerable() 三个用来判断对象中属性不否存在的区别：
 */
"x" in obj; // obj自有属性或继承属性中包含x则返回true
obj.hasOwnPrepertyOf('x'); // 仅obj自有属性中包含x时返回true （此方法用来检测属性是否是自有属性）
obj.propertyIsEnumerable('x'); // hasOwnPrepertyOf的增强版，在x为obj自有属性且x的enumerable值为true时才返回true
obj.x !== undefined; // 最简单判断obj是否具有x属性的方式, 此方法不能区分x属性存在且值为undefined的情况 obj = {x: undefined}; 时返回false 
obj.x != null; // 可用判断objx不为null和undefined的情况

/**
 * for in: 遍历本身所有可以枚举的属性（包括自有属性和继承属性）
 * 针对数组： for in遍历的是数组的索引（即键名），且遍历顺序有可能不是按照实际数组的内部顺序,而是以原始插入顺序迭代对象的可枚举属性，所以更适合遍历对象
 * 
 */
for (var index in myArray) {
  console.log(myArray[index]);
}

/**
 * for of: 对可迭代对象( Array，Map，Set，String，TypedArray，arguments 对象等等)执行遍历，同时调用自定义fn函数
 * 针对数组：for of遍历的是数组元素值(数组自身值，不包含数组原型上的内容)
 * for of: 不支持普通对象
 * 由break, throw  continue    或return终止
 */
for (let o of foo ){ 
  console.log(o); 
  break; // closes iterator, triggers return
}

/**
 * Object.getOwnPrepertyOfDescriptor(ojb, 'x'): 获取自有属性x的4个特性：value, writable, enumerable, configurable
 */

/** 
 * 给Object.prototype添加一个不可枚举的extend（）方法，这个方法继承自调用它的对象，将作为参数传入的对象的属性一一复制
 * 除了值之外，也复制属性的所有特性，除非在目标对象中存在同名的属性参数对象的所有自有对象（包括不可枚举的属性）也会一一复制      
 */    
Object.defineProperty(Object.prototype, "extend",{
  writable: true,
  enumerable: false, //将其定义为不可枚举的
  configurable: true,
  value: function (o) { //值就是这个函数
    //得到所有的自有属性，包括不可枚举属性
    var names = Object.getOwnPropertyNames(o);
    //遍历它们
    for (var i = 0; i < names.length; i++) {
      //如果属性已经存在，则跳过
      if (names[i] in this) continue;
      //获得o中的属性的描述符
      var desc = Object.getOwnPropertyDescriptor(o, names[i]);
      //用它给this创建一个属性
      Object.defineProperty(this, names[i], desc);
    }
  }
});

/**
 * 对象序列化与反序列化：
 * 对象序列化： 将对象转换为string, 通过JSON.stringify()来实现序列化(底层调用toJSON()来实现的)：该方法只能序列化对象的可枚举属性(序列化不支持RegExp对象, 函数，undefined 和 Error对象)
 * 反序列化：通过JSON.parse()将字符串转换为对象
 * 一般通过这两种方法组合实现深拷贝：
 * 
 */
let obj = {
  name: 'jhon',
  arr: [1,2,3],
  getBack: function(){console.log('函数拷贝不了')}
}
let newObj = JSON.parse(JSON.stringify(obj));

newObj.arr == obj.arr; // false, 两个对象并未指向同一个数组地址
// 缺点：1、对象里有函数,函数无法被拷贝下来，2、无法拷贝obj对象原型链上的属性和方法.
// 深拷贝的实现：
// 1、遍历那个被拷贝的对象，2、判断对象里每一项的数据类型 3、如果不是对象类型,就直接赋值,如果是对象类型,就再次调用deepCopy,递归的去赋值
function deepCopy(sourceObj, newObj){
  // 首先要排除sourceObj是数组的情况

  for(var key in sourceObj){
    // 只拷贝自有属性，继承过来的不管
    if(sourceObj.hasOwnPrepertyOf(key)){
      // 如果这一项是object类型,就递归调用deepCopy
      if(typeof(source[key]) === "object"){
        newObj[key] = Array.isArray(source[key]) ? [] : {};
        deepCopy(sourceObj[key], newObj[key]);
      }else{
        newObj[key] = sourceObj[key];
      }
    }
  }
}

/**
 * 防抖：当持续触发事件时，一定时间段内没有再触发事件，事件处理函数才会执行一次，如果设定的时间到来之前，又一次触发了事件，就重新开始延时。
 * 即：将几次操作合并为一此操作进行， 场景：实时搜索，拖拽
 * 实现原理如下：
 * 1、维护一个定时器，在延时500后触发函数，
 * 2、如果未到500的时间又再次触发则清除上一次定时器，重新设置。
 * 3、最终 只有最后一次操作会触发，其它的均会被拦截取消。
 */  
window.onscroll = function(){
  let timer = null;
  return function(){
    // 多次触发时，清除上一次定时器
    if(timer !== null){
      clearTimeout(timer);
    }
    // 然后重设定时器
    timer = setTimeout(function(){
      console.log('500毫秒执行一次！');
    }, 500)
  }
}

/**
 * 节流: 合并函数触发次数，使得一定时间内只触发一次函数
 * 即：一个函数只有在大于等于执行周期时才会执行，周期内不执行。场景：窗口调整（调整大小），页面滚动（滚动），抢购时疯狂点击（鼠标按下）
 * 实现原理如下：
 * 1、记录上次触发时间点
 * 2、记录当前时间点并计算
 * 3、
 */
window.onresize = function(){
  let lastTimer = 0;
  return function(){
    // 获取当前时间
    let nowTimer = new Date().getTime();
    // 触发间隔大于等于执行周期时才执行事件
    if( nowTimer - lastTimer >= 1000){
      console.log('执行触发操作，同时更新周期时间点');
      // 更新最后时间
      lastTimer = nowTimer;
    }
  }
}














/**********************************************数组方法的解释说明 ************************************/ 
/**
 * slice(start, end): 从原数组中截取从下标start开始至end结束(不包含end)之间的元素，如果end为负值则从数组尾部开如，
 * -1从指定最后一个元素，-3从数组倒数第3个开始
 */
let a = [0,1,2,3,4,5,6,7];
a.slice(1, 3); // 输出[1,2] 但数组a保持不变

/**  
 * splice(start, num, a,b,c,d): 修改原数组，在下标start的位置(从0开始)，添加或删除的个数num, 后续跟的是要添加的元素, 返回被删除的数组
 * 如果 start 为负值 则从原数组倒数第start个位置开始， 如果 start绝对值 > 原数组长度，则从0下标开始
 * 如果 num > start之后的总数，则从start位置开始至最后的元素都将被从原数组中删除(包含start位置)
 * 如果 num 被省略，表示从start位置开始直到原数组结尾(从satrt位置开始且包含start位置)
 * 如果 num 是 0 或者负数，则不移除元素,同时应至少添加一个元素(在satrt位置之后开始插入)
*/
let a = [0,1,2,3,4,5,6];
a.splice(4, 30); // 输出[4,5,6] a变为[0,1,2,3]
a.splice(4); // 输出[4,5,6] a变为[0,1,2,3]
a.splice(4,0,7);// 输出[] a变为[0,1,2,3,7,4,5,6]
a.splice(4,0,7,[8,8]); //输入[] a变为[0,1,2,3,7,[8,8],4,5,6]  不会将添加的数组元素拆成单个元素添加到原数组中

/**
 * forEach()不能在所有元素都传递给调用的函数之前终止循环，即不能像for循环那样通过break退出
 * 只能通过try catch抛出forEach.break异常来终止
 */
try{
  arr.forEach(item => {
    if(item > 0){
      // 通过异常终止循环
      throw new Error("StopIteration");
    }
  })
}catch(e){张
  if(e.message == 'StopIteration'){
    console.log('跳出forEach循环了');
  }else{
    throw e;
  }
}
console.log('不影响这里代码的执行')

/**
 * map() 对数组中的每个元素调用一次传递给它的函数，同时返回一个和原数组相同长度的新数组，map不会修改原数组的数组
 * 注意：执行函数只针对有值且有索引的元素，如果从来没被赋过值或者使用 delete 删除的索引则不会执行函数。
 * 传递给map的函数应该具有返回值
 */
arr.map(item =>{
  return item * 3;
})

/**
 * filter() 返回一个新的数组，其为调用数组的子集，不会改变原数组，filter()会跳过数组中的空元素(undefined)
 *  可用来压缩空缺或删除数组中的null及undefined元素
 */
arr.filter(item => {
  return item != null || item != undefined;
})

/**
 * every() 为数组中每一个元素执行函数调用，如果任何一个执行结果为false则立即返回false, 全部执行结果为true时才返回true(停止遍历)
 * 没有被赋值或已经删除的索引不会被调用。
 * 对于放在空数组上的任何条件，every()函数都只返回true
 */

arr.every(item => { return false;}) // 结果为ture;
let arr = [1,2,3,4,5,6];
let result = arr.every(item => {
  return item > 5;
})// 结果为false, 因为不是所有的元素都>5

/**
 * some(): 刚好与every相反，仅当全部元素的执行结果为false时才返回false, 任何一个元素的执行结果如果为true，则会立即返回true(停止遍历)
 * 对于放在空数组上的任何条件，some()函数都只返回false
 */
arr.some(item => { return false; })// 结果为false;

let arr = [1,2,3,4,5,6];
let result = arr.some(item => {
  return item > 5;
})// true，因为存在>5的数

/**
 * reduce(): 使用指定函数对数组进行组合，生成单个值，又称为注入或折叠, 接收2个参数：
 * 参数1：fn 用来对数组进行组合操作；参数2：传递给函数的初始值, 同时上一次执行的结果也会作为初始值传入下一次调用，不指定初始值时会将数组的第一个值作为默认的初始值
 * 空数组调用时，若无初始值则报类型错误异常
 */
let arr = [1,2,3,4,5];
let sum = arr.reduce(function(x, y){
  return x + y; // 每次的结果都将作为新的初始值传递给下一次调用
}, 0); // 输出 15(求和)
let max = arr.reduce(function(x, y){
  return x > y? x: y;
}); // 输出 5（求最大值）

/**
 * reduceRight(): 从数组右侧倒序开始对数组进行组合，生成单个值
 */

// 合并2个对象，如果有同名的key，则使用第一个key值 
function union(o, p){
  return extend(extend({}, o), p);
}
let objects = [{x:1,a:1}, {x:2,y:2},{z:3,a:3}];
// 两者的结果不同
let result1 = objects.reduce(union); // [{x:1},{y:2},{z:3},{a:1}]
let result2 = objects.reduceRight(union); // [{x:1},{y:2},{z:3},{a:3}]

/**
 * 类数组：如anguments，document.getElementsByTag()的结果等均为类数组，具有length属性和真正数组的一些方法，其类属性为"Array"
 * 类数组调用 Array.isArray() 返回false;
 * Array.from()可以将一个类数组转化为真正的数组
 */
function isArrayLike(o){
 if(
   o && // 非null,undefined
   typeof o === 'object' && // o为对象 
   isFinite(o.length) && // 且长度为有限数组
   o.length >= 0 &&  // 长度为正数
   o.length === Math.floor(o.length) && // 长度为整数
   o.length < 4294967296 && // 长度小于2^32
   o.nodeType != 3 // 排除dom文本节点
 ){
  return true; // 同时满足以上条件则为类数组对象
 }else{
   return false;
 }
}// 判断数组是否为类数组


/**********************************************函数的解释说明 ************************************/ 

/**
 * 函数中关于作用域和作用域链的说明 
 * 在不包含嵌套的函数体内，作用域链上有两个对象：1、是定义函数参数和局部变量的对象，2、是全局对象(非闭包情形)
 * 在一个嵌套的函数体内，作用域上至少有三个对象：
 * 一个嵌套的函数：在每次调用外部函数的时候，内部函数又会重新定义一遍。因为每次调用外部函数的时候，作用域链都是不同的。
 * 内部函数在每次定义的时候都要微妙的差别---在每次调用外部函数时，内部函数的代码都是相同的，而且关联这段代码的作用域链也不相同。
 * 
 */

 function test(name){
  console.log(name); // 输出：one(参数)
  name="two";
  console.log(name); // 输出：two
 }
 var name = 'one';
 test(name);
 console.log(name); //输出：one 基本类型是按值传递的，函数调用并未改变基本类型的值

/**
 * 作用域链的例子: 嵌套函数 
 * 嵌套函数作用域链上有三个对象: 在调用的时候，需要查找name的值，就在作用域链上查找
 * 
 */ 
var name = 'one';
function test(){
  var name = 'two';
  function test1(){
    var name = 'three';
    console.log(name); // 输出 three
  }
  function test2(){
    console.log(name); // 输出 two
  }
  // 在函数体内部执行test1、 test2
  test1(); // 1、当成功调用test1()的时候，顺序为 test1()->test()->全局对象window, 由于test1上能找到name：three,所以直接返回
  test2(); // 2、当成功调用test2()的时候，顺序为 test2()->test()->全局对象window, 由于test2上没有找到name,所以继续往上在test中找：找到name，值为two，所以直接返回
}

// 在外部通过执行test() 查看结果
test(); // 输出：three two

var temp = 1;
if(true){ //定义了一个代码块：ES5中并不存在代码，只是通过此种方式来定义
  var temp = 2; // 在块中定义了一个变量，它的作用域链的第一个对象就是全局对象window
  console.log(this); // 代码块中，this指向window
}; 
alert(temp); // 输出 2，

/**
 * 1、使用对象字面量创建的对象，其__proto__值是 Object.prototype。
 * 2、使用数组字面量创建的对象，其__proto__值是 Array.prototype。
 * 3、使用 function f(){} 函数创建的对象，其__proto__值是 Function.prototype
 * 4、使用 new fun() 创建的对象，其中 fun 是由 JavaScript 提供的内建构造器函数之一(Object, Function, Array, Boolean, Date, Number, String 等等），其__proto__值是 fun.prototype。
 * 5、使用其他 JavaScript 构造器函数创建的对象，其__proto__值就是该构造器函数的 prototype 属性
 *  
 */

let o = {a: 1}; // 1、对象字面量创建的对象，原型链:	o ---> Object.prototype ---> null
let a = ["yo", "whadup", "?"]; // 2、数组字面量创建的对象， 原型链:a ---> Array.prototype ---> Object.prototype ---> null
function f(){return 2;} // 3、原型链:	f ---> Function.prototype ---> Object.prototype ---> null
var fun = new Function(); // 4、原型链:	fun ---> Function.prototype ---> Object.prototype ---> null
function Foo() {return {};}
let foo = new Foo(); // 5、原型链:	foo ---> Foo.prototype ---> Object.prototype ---> null



/**
 * 解释闭包的三条理伦：
 * 1、js作用域只和函数的界定符相关，函数与函数的嵌套形成了作用域链；
 * 2、作用域链的创建规则是复制上一层环境的作用域链，并将指向本环境变量对象的指针放到链首；
 * 3、在Javascript中，如果一个对象不再被引用，那么这个对象就会被GC回收。如果两个对象互相引用，而不再被第3者所引用，那么这两个互相引用的对象也会被回收。
 * GC回收规则：
 */
// 闭包函数
var temp = (function(){
  var num = 10;
  return function(){
    num ++;
    console.log(num); // 输出 test
  }
})();

/**********************************************new语法糖的解释说明 ************************************/ 
// 需求：批量创建一百个士兵obj?
/**
 * 不使用new的实现方法：
 */
createSoldier.prototype = {
  兵种: '美国大兵',     
  攻击力: 5,
  攻击: function(){console.log('攻击')},
  防御: function(){console.log('防御')},
  死亡: function(){console.log('死亡')},
}
function createSoldier(){
  var obj = {
    ID: 1,
    生命值: 42
  }  //  1、new的时候偷偷创建了一个空对象
  obj.__proto__ = createSoldier.prototype // 2、让this的__proto__ 指向了构建函数createSoldier的原型即createSoldier.prototype
  return obj; // 3、偷偷return了 this
}
Soldier.prototype = { constructor : Soldier}; // 4、偷偷给构造函数的prototype加了个constructor属性
// 通过循环创建
var soldiers = []
for(var i = 0 ;i<100 ; i++){
  soldiers.push(createSoldier()) 
}

/**
 * 使用new的实现方式：通过上下对比： new做了4件事：
 * 1、偷偷的new了一个空对象 
 * 2、让this的__proto__ 指向 构造函数的原型
 * 3、将对象指给当前的this并return这个this
 * 4、偷偷给构造函数的prototype加了个constructor属性
 * 
 * 总结下来就是：js帮你创建空对象,帮你搞定this指向,帮你改变this的__proto__,帮你return this。
 */
 function Soldier(){
  this.ID  = 1
  this.生命值 = 42
}
Soldier.prototype.兵种 = '美国大兵'
Soldier.prototype.攻击力 = '65'
Soldier.prototype.攻击 = function(){console.log('攻击')}
Soldier.prototype.防御 = function(){console.log('防御')}
Soldier.prototype.死亡 = function(){console.log('死亡')}
// 通过循环创建
var soldiers = []
 for(var i = 0 ;i<100 ; i++){
     soldiers.push(new Soldier()) 
 }

 /**********************************************原型链继承的通俗说明 ************************************/

//  正常渠道实现继承
function Human(options) {
  this.name = options.name;
  this.age = options.age;
}
Human.prototype.eat = function(){};
Human.prototype.drink = function(){};
function Soldier(options){
  Human.call(this,options); // this代表当前调用， 将this绑定到Human上，从而获得Human里的自有属性name和age  
  this.id = options.id;
}
Soldier.prototype.__proto__ = Human.prototype; // 为了得到Human里的公有属性eat和drink; __proto__有很严重的性能问题。
Soldier.prototype.攻击力 = 5
Soldier.prototype.兵种 = '美国大兵'
Soldier.prototype.攻击 = function () {}
Soldier.prototype.防御 = function () {}
Soldier.prototype.死亡 = function () {}

// 创建实例
var soldier = new Soldier({name:'ziwei', age: 19, id:'8'});
 
/**
 * 由于__proto__不允许在生产环境使用,文档明确有要求的,影响性能，所以通过new语法糖修改上述实现方案 
 * 主要是通过new来改写这一步的实现： Soldier.prototype.__proto__ = Human.prototype; 其实现方法如下：
 * function Fn(){} 
 * Fn.prototype = Human.prototype
 * Soldier.prototype = new Fn(); 
 * 理由如下：
 * Soldier.prototype是Fn构造函数的实例，所以Soldier.prototype.__proto__ ==== Fn.prototype
 * Fn.prototype === Human.prototype;
 * 所以：Soldier.prototype.__proto__ === Human.prototype
 * 最图 new的实现方案如下：
 * 
 */
// new实现继承
function Human(options) {
  this.name = options.name;
  this.age = options.age;
}
Human.prototype.eat = function(){};
Human.prototype.drink = function(){};
function Soldier(options){
  Human.call(this,options); // this代表当前调用， 将this绑定到Human上，从而获得Human里的自有属性name和age  
  this.id = options.id;
}
function Fn(){}
Fn.prototype = Human.prototype
Soldier.prototype = new Fn(); // 其它均不变，通过这三句代码实现 Soldier.prototype.__proto__ ==== Fn.prototype

Soldier.prototype.攻击力 = 5
Soldier.prototype.兵种 = '美国大兵'
Soldier.prototype.攻击 = function () {}
Soldier.prototype.防御 = function () {}
Soldier.prototype.死亡 = function () {}

// 创建实例
var soldier = new Soldier({name:'ziwei', age: 19, id:'8'});

// ES5提供了Object.create()，通过此方法可以对上面的继承进行优化
// Object.create()实现继承
function Human(options) {
  this.name = options.name;
  this.age = options.age;
}
Human.prototype.eat = function(){};
Human.prototype.drink = function(){};
function Soldier(options){
  Human.call(this,options); // this代表当前调用， 将this绑定到Human上，从而获得Human里的自有属性name和age  
  this.id = options.id;
}
Soldier.prototype = Object.create(Human.prototype); // 继承Human的共有属性，通过这一方法实现 new语法糖三句代码的效果。

Soldier.prototype.攻击力 = 5
Soldier.prototype.兵种 = '美国大兵'
Soldier.prototype.攻击 = function () {}
Soldier.prototype.防御 = function () {}
Soldier.prototype.死亡 = function () {}

// 创建实例
var soldier = new Soldier({name:'ziwei', age: 19, id:'8'});


 /**********************************************事件循环机制的说明 ************************************/

/**
 * JS的执行机制是
 * 首先判断JS是同步还是异步,同步就进入主线程,异步就进入event table
 * 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
 * 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中
 */

setTimeout(function(){
  console.log('第4步')
});

new Promise(function(resolve){
  // new Promise 是同步任务,被放到主线程里
  console.log('第1步');
  for(var i = 0; i < 10000; i++){
      i == 99 && resolve();
  }
}).then(function(){
  // .then里的函数是 异步任务,被放到event table
  // Promise中的异步体现在then和catch中
  console.log('第3步')
});

console.log('第2步');

// 上述代码的执行顺序：'第1步 '第2步 '第3步 '第4步 

/**
 * 
 * 准确的划分方式是:
 * macro-task(宏任务)：包括：script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)
 * micro-task(微任务)：Promise.then、MutaionObserver、process.nextTick(Node.js 环境)
 * 
 * 按照这种分类方式:JS的执行机制是
 * 1、执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里
 * 2、当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完，确切来说是当前宏任务完成，下次宏任务开始前
 * 所以上述代码的解释如下：
 * 1、首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
 * 2、遇到 new Promise直接执行,打印"第1步"
 * 3、遇到then方法,是微任务,将其放到微任务的【队列里】
 * 4、打印 "第2步"
 * 5、本轮宏任务执行完毕,查看本轮的微任务(由事件触发线程维护),发现有一个then方法里的函数, 打印"第3步"
 * 6、到此,本轮的event loop 全部完成
 * 7、下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数(定时器线程维护),执行打印"第4步"
 * 
 * 
 * 针对ES6及ES7的语法：async await有如下说明 ：
 * 1、async await 本身就是promise+generator的语法糖。所以await后面的代码是microtask。
 * 2、执行到await时会将await后面的表达式会先执行一遍，将await后面的代码加入到microtask中，然后就会跳出整个async函数来执行后面的代码
 */

/**********************************************模拟call,apply及bind的实现 ************************************/

// 实现call(obj,arg,arg....): 将目标函数的this指向传入的第一个对象，参数为不定长，且立即执行
// 实现思路： 1、改变this指向：可以将目标函数作为这个对象的属性，2、利用arguments类数组对象实现参数不定长，3、不能增加对象的属性，所以在结尾需要delete
Function.prototype.myCall = function(obj){
  var args = args = [...arguments].slice(1);
  obj.fn = this; // 1、为当前对象添加一个函数fn, 值为要已经定义的要调用的函数
  obj.fn.apply(obj,args); // 2、执行添加的函数fn
  delete  obj.fn; // 3、执行完以后删除
}

// 实现apply（）：apply第二个参数为数组
Function.prototype.myApply = function(obj, arr){
  obj.fn = this; // 1、为当前对象添加一个函数fn, 值为要已经定义的要调用的函数
  if(!arr){
    obj.fn();
  }else{
    var args = []; 
    for(var i = 0; i < arr.length; i++) {
      args.push('arr[' + i + ']');
    }
    // eval('obj.fn('+args+')'); // 旧办法
    obj.fn(...args);
  }
  delete  obj.fn;
}

// bind实现：返回一个与被调函数具有相同函数体的新函数，且这个新函数也能使用new操作符。
// 实现思路：1、返回一个函数，其他与call, apply类似， 2、如果返回的函数作为构造函数，bind时指定的 this 值会失效，但传入的参数依然生效。
Function.prototype.myBind = function(obj) {
  var that = this;
  var args = [...arguments].slice(1);
  // 作为构造函数使用
  var Fbind  = function(){
    var self = this instanceof Fbind ? this : obj
    return that.apply(self, args.concat(...arguments));
  };
  //创建一个中转函数fn，让Fbind间接继承目标函数的原型
  var fn =  function(){};
  fn.prototype= that.prototype;
  Fbind.prototype= new fn(); 
}